﻿using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public sealed class XEffectManager
{
    public static XEffectManager Instance { get; internal set; }

    static XEffectManager()
    {
        Instance = new XEffectManager();
        Instance.InitialCacheStrategy();
    }

    private class CacheBase
    {
        public string effectPath;
        public GameObject prefab;
        public bool loading = false;
        public float createTime = 0f;

        public virtual void Release()
        {
            prefab = null;
            loading = false;
            createTime = 0f;
        }
    }

    private class XEffectCache : CacheBase
    {
        public Queue<XEffectComponent> queue;

        public override void Release()
        {
            if (queue != null)
            {
                while (queue.Count > 0)
                {
                    XEffectComponent efc = queue.Dequeue();
                    if (efc != null && efc.gameObject != null)
                    {
                        GameObject.Destroy(efc.gameObject);
                    }
                }

                queue.Clear();
                queue = null;
            }

            base.Release();
        }
    }

    public Transform objectContainer = null;
    
    private int effectGlobalId = 1;
    private int maxEffectCacheCount = 16;
    private int maxEffectQueueCount = 8;
    private float effectQueueTimeThreshold = 10f;

    private List<XEffectCache> cacheList = new List<XEffectCache>();
    private Dictionary<int, XEffectComponent> effectMap = new Dictionary<int, XEffectComponent>();
    private Dictionary<string, int> maxEffectCountDic = new Dictionary<string, int>();
    private Dictionary<string, int> currentEffectCountDic = new Dictionary<string, int>();

    public void InitialCacheStrategy()
    {
        maxEffectCacheCount = 16;
        maxEffectQueueCount = 8;
        effectQueueTimeThreshold = 20f;
    }

    private void AddEffectMap(XEffectComponent effect)
    {
        if (effect == null) return;
            effect.gid = effectGlobalId++;

        effectMap.Add(effect.gid, effect);
        AddEffectCount(effect.resPath);
    }

    private bool CanEffectInstantial(string effectPath)
    {
        if (currentEffectCountDic.ContainsKey(effectPath) && currentEffectCountDic[effectPath] >= maxEffectCountDic[effectPath])
        {
            return false;
        }
        return true;
    }

    private void AddEffectCount(string effectPath)
    {
        if (maxEffectCountDic.ContainsKey(effectPath))
        {
            if (!currentEffectCountDic.ContainsKey(effectPath))
                currentEffectCountDic[effectPath] = 0;
            currentEffectCountDic[effectPath] += 1;
        }
    }

    private void RemoveEffectCount(string effectPath)
    {
        if (currentEffectCountDic.ContainsKey(effectPath) && currentEffectCountDic[effectPath] > 0)
        {
            currentEffectCountDic[effectPath] -= 1;
        }
    }

    public void SetMaxEffectCount(string effectPath, int maxCount)
    {
        maxEffectCountDic[effectPath] = maxCount;
    }

    private void AddEffectCache(string path, XEffectCache cache)
    {
        AddCache<XEffectCache>(cacheList, maxEffectCacheCount, path, cache);
        ReleaseAllEffectQueueInCacheByTimeThreshold();
    }

    private void AddCache<T>(List<T> cacheList, int maxCacheCount, string path, T cache) where T : CacheBase
    {
        cache.createTime = Time.time;
        cache.effectPath = path;
        if (cacheList.Count < maxCacheCount)
        {
            cacheList.Add(cache);
        }
        else
        {
            cacheList.Sort(delegate (T x, T y)
            {
                return (int)(x.createTime - y.createTime);
            });
            cacheList[0].Release();
            cacheList[0] = cache;
        }
    }

    private void RemoveCache<T>(List<T> cacheList, T cache) where T : CacheBase
    {
        if (cache != null && cacheList.Contains(cache))
        {
            cacheList.Remove(cache);
        }
    }

    private void RemoveCache<T>(List<T> cacheList, string path) where T : CacheBase
    {
        T cache = cacheList.Find(delegate (T obj)
        {
            return obj.effectPath == path;
        });

        if (cache != null)
        {
            cacheList.Remove(cache);
        }
    }

    // get cache
    private bool TryGetEffectCache(string path, out XEffectCache effectCache)
    {
        return TryGetCache<XEffectCache>(cacheList, path, out effectCache);
    }

    private bool TryGetCache<T>(List<T> cacheList, string path, out T cache) where T : CacheBase
    {
        T effectInCache = cacheList.Find(delegate (T obj) {
            return obj.effectPath == path;
        });
        cache = effectInCache;
        if (cache != null)
        {
            cache.createTime = Time.time;
        }
        return cache != null;
    }

    // release queue
    private void ReleaseAllEffectQueueInCacheByTimeThreshold()
    {
        for (int i = 0; i < cacheList.Count; i++)
        {
            if ((Time.time - cacheList[i].createTime) > effectQueueTimeThreshold && cacheList[i].queue != null)
            {
                while (cacheList[i].queue.Count > maxEffectQueueCount)
                {
                    XEffectComponent efc = cacheList[i].queue.Dequeue();
                    GameObject.Destroy(efc.gameObject);
                }
            }
        }
    }

    private bool CheckReleaseQueueInCacheByTimeThreshold(XEffectCache cache, float timeThreshold)
    {
        bool canEnqueue = true;
        if ((Time.time - cache.createTime) > timeThreshold && cache.queue != null)
        {
            while (cache.queue.Count > maxEffectQueueCount)
            {
                XEffectComponent efc = cache.queue.Dequeue();
                GameObject.Destroy(efc.gameObject);
                canEnqueue = false;
            }
        }
        return canEnqueue;
    }

    private bool CheckReleaseQueueInCacheByTimeThreshold(XEffectCache cache)
    {
        return CheckReleaseQueueInCacheByTimeThreshold(cache, effectQueueTimeThreshold);
    }

    private XEffectComponent GenerateEffect(string effectPath, XEffectCache effectCache)
    {
        XEffectComponent effect = null;
        if (effectCache.queue.Count > 0)
        {
            effect = effectCache.queue.Dequeue();
            if (effect == null || effect.gameObject == null)
            {
                effect = null;
            }
            else
            {
                effect.gameObject.SetActive(true);
            }
        }
        else
        {
            if (effectCache.prefab == null)
            {
                RemoveCache<XEffectCache>(cacheList, effectCache);
                return null;
            }

            if (!CanEffectInstantial(effectPath))
                return null;

            GameObject obj = GameObject.Instantiate(effectCache.prefab) as GameObject;
            effect = obj.GetComponent<XEffectComponent>();
            if (effect != null)
                effect.resPath = effectPath;
        }

        AddEffectMap(effect);
        return effect;
    }

    private IEnumerator GetEffect(string effectPath, System.Action<XEffectCache, XEffectComponent> callback)
    {
        XEffectCache effectCache = null;
        XEffectComponent effect = null;

        TryGetEffectCache(effectPath, out effectCache);
        if (effectCache == null)
        {
            effectCache = new XEffectCache();
            effectCache.loading = true;
            AddEffectCache(effectPath, effectCache);

            yield return XRes.LoadAsync<GameObject>(effectPath, delegate (Object obj) {
                effectCache.queue = new Queue<XEffectComponent>();
                effectCache.prefab = obj as GameObject;
                effectCache.loading = false;
            });
        }

        while (effectCache.loading)
            yield return null;

        if (effectCache.prefab != null)
        {
            effect = GenerateEffect(effectPath, effectCache);
        }

        callback(effectCache, effect);
    }

    private IEnumerator GetEffect(GameObject effectPrefab, System.Action<XEffectCache, XEffectComponent> callback)
    {
        XEffectCache effectCache = null;
        XEffectComponent effect = null;

        string effectPath = effectPrefab.name;
        TryGetEffectCache(effectPath, out effectCache);
        if (effectCache == null)
        {
            effectCache = new XEffectCache();
            effectCache.loading = true;
            AddEffectCache(effectPath, effectCache);

            effectCache.queue = new Queue<XEffectComponent>();
            effectCache.prefab = effectPrefab;
            effectCache.loading = false;
        }

        while (effectCache.loading)
            yield return null;

        if (effectCache.prefab != null)
        {
            effect = GenerateEffect(effectPath, effectCache);
        }

        callback(effectCache, effect);
    }

    public void PreloadEffectPrefabs(string[] resList, System.Action<bool> callback)
    {
        XCoroutine.Run(DoPreloadEffectPrefabs(resList, callback));
    }

    private IEnumerator DoPreloadEffectPrefabs(string[] resList, System.Action<bool> callback)
    {
        List<string> resLoad = new List<string>();
        for (int i = 0; i < resList.Length; i++)
        {
            string effectPath = resList[i];
            XEffectCache effectCache = null;
            TryGetEffectCache(effectPath, out effectCache);
            if (effectCache == null)
            {
                effectCache = new XEffectCache();
                AddEffectCache(effectPath, effectCache);
            }

            if (!effectCache.loading)
            {
                effectCache.loading = true;
                resLoad.Add(effectPath);
            }
        }

        yield return XRes.LoadMultiAsync(resLoad.ToArray(), delegate (Object[] obj) {
            for (int i = 0; i < obj.Length; i++)
            {
                XEffectCache effectCache = null;
                if (TryGetEffectCache(resLoad[i], out effectCache))
                {
                    effectCache.prefab = obj[i] as GameObject;
                    effectCache.queue = new Queue<XEffectComponent>();
                    effectCache.loading = false;
                }
            }

            if (callback != null)
                callback(true);
        });
    }

    public void DestroyEffect(XEffectComponent effect, bool destroyed = false)
    {
        if (effect == null) return;

        if (!destroyed)
        {
            XEffectCache effectCache = null;
            TryGetEffectCache(effect.resPath, out effectCache);
            if (effectCache != null)
            {
                if (effectCache.queue == null)
                    effectCache.queue = new Queue<XEffectComponent>();
                effectCache.queue.Enqueue(effect);
                effect.transform.parent = objectContainer;
                effect.gameObject.SetActive(false);
            }
            else if (effect.gameObject != null)
            {
                GameObject.Destroy(effect.gameObject);
            }
        }

        if (effect.gid > 0)
        {
            effectMap.Remove(effect.gid);
            effect.gid = 0;
            RemoveEffectCount(effect.resPath);
        }
    }

    public void DestroyEffect(int effectId)
    {
        XEffectComponent effect = null;
        effectMap.TryGetValue(effectId, out effect);
        if (effect != null)
            DestroyEffect(effect, false);
    }

    public static void Destroy(int effectId)
    {
        Instance.DestroyEffect(effectId);
    }

    private void DoGenerateAsync(string effectPath, System.Action<XEffectCache, XEffectComponent> callback)
    {
        XEffectCache effectCache = null;

        TryGetEffectCache(effectPath, out effectCache);

        if (effectCache != null && !effectCache.loading)
        {
            callback(effectCache, GenerateEffect(effectPath, effectCache));
        }
        else
        {
            XCoroutine.Run(GetEffect(effectPath, callback));
        }
    }


    public void ClearCache()
    {
        if (objectContainer != null)
        {
            foreach (Transform child in objectContainer)
            {
                GameObject.Destroy(child.gameObject);
            }
        }

        cacheList.Clear();
        effectMap.Clear();
        currentEffectCountDic.Clear();
    }

    public void ClearQueueInCache()
    {
        for (int i = 0; i < cacheList.Count; i++)
        {
            if (cacheList[i].queue != null)
            {
                while (cacheList[i].queue.Count > 0)
                {
                    XEffectComponent eff = cacheList[i].queue.Dequeue();
                    if (eff != null && eff.gameObject != null)
                        GameObject.Destroy(eff.gameObject);
                }
            }
        }

        currentEffectCountDic.Clear();
    }

    public static void GenerateAsyncEffect(GameObject owner, Vector3 position, Quaternion rotation, string effectPath, float time,
        System.Action<XEffectComponent> callback)
    {
        Instance.DoGenerateAsync(effectPath, delegate (XEffectCache effectCache, XEffectComponent effect) {
            if (effect != null)
                effect.Initialize(owner, position, rotation, effectCache.prefab, time);
            
            if (callback != null)
                callback(effect);
        });
    }

    public static void PlayAsyncEffect(GameObject owner, Vector3 position, Quaternion rotation, string effectPath, float time,
        System.Action<XEffectComponent> callback)
    {
        XRes.LoadAsync(effectPath, typeof(XEffectConfigObject),(config) =>
        {
            XEffectConfigObject configObject = (XEffectConfigObject)config;
            if (configObject.effectFiles.Length != 0)
            {
                int level = configObject.effectFiles.Length - 1;
                if (configObject.follow == XEffectComponent.EffectFollowType.Both)
                {
                    GenerateAsyncEffect(owner,configObject.getRes(level), time, callback);
                }
                else
                {
                    GenerateAsyncEffect(owner, position, Quaternion.identity,
                        configObject.getRes(level), time, callback);
                }
            }
        });
    }


    public static void GenerateAsyncEffect(GameObject parent, string effectPath, float time, 
        System.Action<XEffectComponent> callback)
    {
        Instance.DoGenerateAsync(effectPath, delegate (XEffectCache effectCache, XEffectComponent effect) {
            if (effect != null)
            {
                effect.Initialize(parent, effectCache.prefab, time);
            }
            
            if (callback != null)
                callback(effect);
        });
    }

    public static void GenerateAsyncEffect(GameObject effectPrefab, float time, 
        System.Action<XEffectComponent> callback)
    {
        XCoroutine.Run(Instance.GetEffect(effectPrefab, delegate (XEffectCache effectCache, XEffectComponent effect) {
            if (effect != null)
                effect.Initialize(effect.gameObject, effectCache.prefab, time);
            
            if (callback != null)
                callback(effect);
        }));
    }

    public static XEffectComponent GetEffect(int gid)
    {
        XEffectComponent effect = null;
        Instance.effectMap.TryGetValue(gid, out effect);

        return effect;
    }

    public static void GenerateAsyncLine(GameObject parent, 
        Vector3 offset, Transform[] targets, string path, XEffectLineMode mode, float time, System.Action<XEffectLineRender> callback)
    {
        XRes.LoadAsync<GameObject>(path, delegate (Object prefab) {
            GameObject lineGo = GameObject.Instantiate(prefab) as GameObject;
            lineGo.transform.SetParent(parent.transform, false);
            lineGo.transform.position = parent.transform.position + offset;

            XEffectLineRender render = lineGo.GetComponent<XEffectLineRender>();
            render.Fire(targets, time, mode);

            if (callback != null)
            {
                callback(render);
            }
        });
    }

    public static void GenerateAsyncLine(GameObject parent,
       Transform[] targets, string path, XEffectLineMode mode, float time, System.Action<XEffectLineRender> callback)
    {
        XRes.LoadAsync<GameObject>(path, delegate (Object prefab) {
            GameObject lineGo = GameObject.Instantiate(prefab) as GameObject;
            lineGo.transform.SetParent(parent.transform, false);
            lineGo.transform.position = parent.transform.position;

            XEffectLineRender render = lineGo.GetComponent<XEffectLineRender>();
            render.Fire(targets, time, mode);

            if (callback != null)
            {
                callback(render);
            }
        });
    }

    public static void GenerateAsyncUVChain(GameObject parent,
        Transform startTrans, Transform endTrans, string path, float time, System.Action<XEffectUVChainRender> callback)
    {
        XRes.LoadAsync<GameObject>(path, delegate (Object prefab)
        {
            GameObject lineGo = GameObject.Instantiate(prefab) as GameObject;
            lineGo.transform.SetParent(parent.transform, false);
            lineGo.transform.position = parent.transform.position;

            XEffectUVChainRender render = lineGo.GetComponent<XEffectUVChainRender>();
            render.Fire(startTrans, endTrans, time);

            if (callback != null)
            {
                callback(render);
            }
        });
    }
}
